using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO; 

namespace HycalperConsole {
    public class HycalperConsole {

        char[] WHITESPACE = new char[4] { '\n', ' ', '\t','\r' };
        public static readonly string LOOPSTART = "#region HYCALPER LOOPSTART";
        public static readonly string LOOPEND = "#endregion HYCALPER LOOPEND";
        public static readonly string TYPELIST = "!HC:TYPELIST:";
        public static readonly string ENUMPATTERN = "!HC:ENUM:";
        public static readonly string ENUMPATTERNEND = "!HC:/ENUM";
        public static readonly string EXCLUDEPATTERN = "HCEXCLUDE";
        public static readonly string AUTOGENERATEDSTART = "#region HYCALPER AUTO GENERATED CODE";
        public static readonly string AUTOGENERATEDEND = "#endregion HYCALPER AUTO GENERATED CODE";
        // if no "endmark" attribute is found for a type, the region 
        // is expected to run, until one of these chars ist found: 
        public static readonly string DEFAULT_ENDREGION_CHARS = "> ,();\n\t\r*[]";  


        private readonly bool include_secure = true; // enclose enums into CDATA tags 
        private readonly bool parenthesize = true; // enclose auto generated source into #region DESC and #endregion tags 
        private readonly bool comment = true;  // comment will be included before first namespace directive (if both was found)
        private readonly bool remove_comments = true;  // all comments beginning with '/*!HC' will be removed from output
                                                       // (this may conflict with 'keep_tags' and has precedence!)
        private readonly bool keep_tags = false; // all hycalper marks will stay in resulting code [false]
        private readonly bool skip_uptodate = true; // only recalp changed files 

        XmlDocument m_xmlDoc; 

        private string m_saveFilename = "";
        public string SaveFilename {
            get {
                return m_saveFilename;
            }
        }
        private Types m_pattern;
        public Types Pattern {
            get {
                return m_pattern;
            }
        }
        private int m_error = 0;
        public int Error {
            get {
                return m_error;
            }
        }
        private string m_message = "";
        public string Message {
            get {
                return m_message;
            }
        }
        private int m_enums = 0;
        public int Enums {
            get {
                return m_enums;
            }
        }
        private int m_loops = 0;
        public int Loops {
            get {
                return m_loops;
            }
        }
        private string m_targetPath = "";

        private int m_linesInsideHCTags = 0;
        public int LinesInsideHCTags {
            get { return m_linesInsideHCTags; }
            set { m_linesInsideHCTags = value; }
        }

        private int m_linesGenerated = 0;
        public int LinesGenerated {
            get { return m_linesGenerated; }
            set { m_linesGenerated = value; }
        }

        private int m_linesGlobal = 0;
        public int LinesGlobal {
            get { return m_linesGlobal; }
        } 
        public HycalperConsole(string filename, string targetDir) {
            try {
                m_targetPath = targetDir;
                int curPosition = 0;
                bool write2same_file = false; 
                string input = System.IO.File.ReadAllText(filename);
                m_linesGlobal = countLines(input); 
                if (input.Length > 0) {
                    // read xml types from input file 
                    int lenXML = 0;
                    m_pattern = readTypesXML(ref input, out lenXML, ref m_xmlDoc, false);
                    if (lenXML == 0) {
                        m_message = "There is nothing to hycalpe here!";
                        m_error = 3;
                        return;
                    }
                    if (m_pattern == null) throw new Exception("Invalid XML Types definition.");
                    // extract outfile
                    XmlNode outFileNameNode = m_xmlDoc.GetElementsByTagName ( "outfile" ).Item ( 0 );
                    if (outFileNameNode != null && outFileNameNode.InnerText != null
                        && outFileNameNode.InnerText.Length > 0) {
                        m_saveFilename = Path.Combine(Path.GetDirectoryName(filename)
                                                           , outFileNameNode.InnerText);
                        if (m_saveFilename == null || m_saveFilename.Length == 0)
                            throw new Exception("Unable to find / extract outfile specification.");
                    } else {
                        // save to self
                        m_saveFilename = filename;
                        write2same_file = true;
                    }
                    // skip if uptodate 
                    if (skip_uptodate && !write2same_file) {
                        DateTime lastAccessOutfile = File.GetLastWriteTime ( m_saveFilename );
                        DateTime lastAccessSource = File.GetLastWriteTime ( filename );
                        if (lastAccessOutfile > lastAccessSource) {
                            // skip 
                            m_message = "...uptodate -> skipped.";
                            m_error = 2; 
                            return; 
                        }
                    }
                    // extract + process loops 
                    curPosition = input.IndexOf(LOOPSTART);
                    int appendIdx = curPosition;
                    while (curPosition >= 0) {
                        int [] excludePattern; 
                        Types localPattern; 
                        string loopSequence = extractLoop(input, ref curPosition, out localPattern, out excludePattern);
                        if (loopSequence != null) {
                            if (localPattern == null) localPattern = m_pattern;
                            System.Array.Sort(excludePattern);
                            appendIdx = beginOfLine(input, curPosition);
                            // if old marks -> remove content between them 
                            if (write2same_file) {
                                int startIdx = input.Substring(0, endOfLine(input, appendIdx)).IndexOf(AUTOGENERATEDSTART, appendIdx);
                                if (startIdx >= 0) {
                                    int endIdx = nextNewLine(input, input.IndexOf(AUTOGENERATEDEND, startIdx));
                                    if (endIdx > startIdx) {
                                        input = input.Remove(appendIdx, endIdx - appendIdx);
                                    }
                                }
                            }
                            // create new region marks
                            if (parenthesize || write2same_file) {
                                input = input.Insert(appendIdx, Environment.NewLine + AUTOGENERATEDEND
                                                                + Environment.NewLine);
                            }
                            int curExcludingIdx = 0;
                            for (int i = 0; i < localPattern.CountPattern; i++) {
                                // replace only pattern which are not excluded
                                if (curExcludingIdx >= excludePattern.Length || excludePattern[curExcludingIdx] != i) {
                                    int tagsReplaced = 0;
                                    string insertString = replaceAllKeys(loopSequence, localPattern, i, ref tagsReplaced);
                                    insertString = removeCommentStubs(insertString, "/*!HC", "*/");
                                    // insert only, if something was replaced
                                    if (tagsReplaced > 0) {
                                        input = input.Insert(appendIdx, insertString);
                                        curPosition += insertString.Length;
                                    }
                                } else {
                                    curExcludingIdx++;
                                }
                            }
                            if (parenthesize || write2same_file) {
                                string endMark = AUTOGENERATEDSTART + Environment.NewLine;
                                if (write2same_file)
                                    endMark = endMark + "// DO NOT EDIT INSIDE THIS REGION !! CHANGES WILL BE LOST !! "
                                                      + Environment.NewLine;
                                input = input.Insert(appendIdx, endMark);
                                curPosition += endMark.Length;
                            }
                            m_loops++;
                        } else {
                            throw new Exception("Error extracting loop sequence: " + m_message);
                        }
                        curPosition = input.IndexOf(LOOPSTART, curPosition + 1);
                    }
                    // find and process enums 
                    curPosition = input.IndexOf(ENUMPATTERN, 0); 
                    while (curPosition >= 0) {
                        int startIdx = input.IndexOfAny(WHITESPACE,curPosition);
                        int startKeyIdx = curPosition + ENUMPATTERN.Length;
                        int endKeyIdx = input.IndexOfAny(new char[11] { ':', ' ', ',', '*', '/', '(', ')', '}', '{', '\r', '\n' }, startKeyIdx);
                        appendIdx = endOfWord(input,endKeyIdx);
                        if (endKeyIdx > startKeyIdx) {
                            string key = input.Substring(startKeyIdx, endKeyIdx - startKeyIdx).Trim();
                            string insertString = enumAllKeyPatterns(key);
                            endKeyIdx = input.IndexOf(ENUMPATTERNEND, endKeyIdx);
                            if (endKeyIdx < 0 )
                                throw new Exception("Missing enum end tag!");
                            endKeyIdx = beginOfWord(input,endKeyIdx); 
                            input = input.Remove(appendIdx, endKeyIdx - appendIdx); 
                            input = input.Insert(appendIdx, " " + insertString + " ");
                            curPosition += insertString.Length;
                            m_enums++;
                        } else {
                            throw new Exception("Missing class spec in enum.");
                        }
                        curPosition = input.IndexOf(ENUMPATTERN, curPosition + 1); 
                    }
                    // Comment 
                    if (comment && input.IndexOf("namespace") > 2) {
                        string commentString;
                        if (write2same_file) {
                            commentString =         "//////////////////////////////////////////////////////////////////"
                            + Environment.NewLine + "//                                                              //"
                            + Environment.NewLine + "//  This is an auto - manipulated source file.                  //"
                            + Environment.NewLine + "//  Edits inside regions of HYCALPER AUTO GENERATED CODE        //"
                            + Environment.NewLine + "//  will be lost and overwritten on the next build!             //"
                            + Environment.NewLine + "//                                                              //"
                            + Environment.NewLine + "//////////////////////////////////////////////////////////////////"
                            + Environment.NewLine;
                        } else {
                            commentString =         "//////////////////////////////////////////////////////////////////"
                            + Environment.NewLine + "//  This is an auto - generated source file.                    //"
                            + Environment.NewLine + "//  Do not manually edit this file! Any changes made will be    //"
                            + Environment.NewLine + "//  lost on the next build! Edit the corresponding source file  //"
                            + Environment.NewLine + "//  (per default located in the template/ folder) instead!      //"
                            + Environment.NewLine + "//                                                              //"
                            + Environment.NewLine + "//////////////////////////////////////////////////////////////////"
                            + Environment.NewLine;
                        }
                        XmlNode commentNode = m_xmlDoc.GetElementsByTagName("comment")[0];
                        if (commentNode != null && commentNode.InnerText != null) {
                            commentString += Environment.NewLine + "///////////////////// UserComment: ///////////////////////////////"
                            + Environment.NewLine + "/*" + commentNode.InnerText + "*/";
                        }
                        if (input.IndexOf(commentString) < 0) {
                            input = input.Insert(0,commentString);
                        }
                    }
                    // write to file
                    if (m_saveFilename != null) {
                        System.IO.File.WriteAllText(m_saveFilename, input);
                    }
                    m_error = 0;    
                    return;
                }
            } catch (Exception e) {
                m_message = e.Message; 
                m_error = 1; 
                return;
            }
        }

        private int countLines (string input) {
            if (input == null || input.Length == 0) 
                return 0; 
            int pos = 0,count = 0;  
            pos = input.IndexOf(Environment.NewLine); 
            while (pos < input.Length && pos >= 0) {
                count++; 
                pos = input.IndexOf(Environment.NewLine,pos+1); 
            }
            return count; 
        }

        private string removeCommentStubs(string insertString, string patternStart, string patternEnd) {
            // remove (de)comments stubs
            if (remove_comments) {
                int startTagIdx = insertString.IndexOf(patternStart, 0);
                while (startTagIdx >= 0) {
                    int endTagIdx = insertString.IndexOf ( patternEnd, startTagIdx );
                    if (endTagIdx > 0) {
                        endTagIdx += patternEnd.Length;
                        if (startTagIdx > 1 && insertString [startTagIdx - 2] == ' '
                            && insertString [startTagIdx - 1] == ' ')
                            startTagIdx--;
                        if (endTagIdx > startTagIdx) {
                            insertString = insertString.Remove ( startTagIdx, endTagIdx - startTagIdx );
                        }
                    }
                    startTagIdx = insertString.IndexOf(patternStart, startTagIdx + 1);
                }
            }
            return insertString;
        }

        private Types readTypesXML(ref string input,out int lengthXML,  ref XmlDocument xmlDoc, bool removeText) {
            lengthXML = 0; 
            int startTagXmlIdx = input.IndexOf("<hycalper>"); //StringComparison.OrdinalIgnoreCase); 
            if (startTagXmlIdx < 0) return null; 
            int endTagXmlIdx = input.IndexOf("</hycalper>", startTagXmlIdx); //,StringComparison.OrdinalIgnoreCase);
            if (endTagXmlIdx > startTagXmlIdx) {
                string xml = input.Substring(startTagXmlIdx, endTagXmlIdx - startTagXmlIdx + "</hycalper>".Length);
                lengthXML = xml.Length; 
                XmlTextReader reader = new XmlTextReader(new StringReader(xml));
                xmlDoc = new XmlDocument();
                xmlDoc.Load(reader);
                // remove xml read from input string 
                if (removeText) {
                    endTagXmlIdx = endOfLine(input, endTagXmlIdx);
                    startTagXmlIdx = beginOfLine(input, startTagXmlIdx);
                    input = input.Remove(startTagXmlIdx, endTagXmlIdx - startTagXmlIdx);
                }
                return new Types(xmlDoc);
            }
            return null; 
        }

        private string extractLoop(string input, ref int curloopstart,out Types localPattern, out int[] excludePattern) {
            string ret = null;
            excludePattern = new int [0];
            localPattern = null; 
            try {
                int startIdx =  input.IndexOf(LOOPSTART, curloopstart) + LOOPSTART.Length;
                startIdx = endOfWord(input,startIdx); 
                int lineEnd = endOfLine(input,startIdx);
                string [] parameter = input.Substring ( startIdx, lineEnd - startIdx ).Trim ().Split(' '); 
                // handle parameter
                bool readingFromFile = false;
                string fileName = "";
                string loopName = "";
                foreach (string param in parameter) {
                    if (param.IndexOf ( '@' ) > 0) {
                        string [] tmpLine = param.Trim().Split ( '@' );
                        if (tmpLine.Length == 2) { 
                            readingFromFile = true;
                            loopName = tmpLine[0].Trim();
                            fileName = tmpLine[1].Trim();
                        } else 
                            Console.Out.WriteLine("INVALID FILE REFERENCE IN LOOP DEFINITION at " + curloopstart); 
                    }
                    if (param.Contains ( EXCLUDEPATTERN )) {
                        string [] tmpLine = param.Trim ().Split ('=', ' ', ',', ':', ';' );
                        if (tmpLine.Length > 0) {
                            if (tmpLine[0].Trim().CompareTo(EXCLUDEPATTERN) != 0) {
                                Console.Out.WriteLine("ERROR PARSING EXCLUDE PATTERN. Exp: " + EXCLUDEPATTERN + "=1,5,7,14");
                                excludePattern = new int[0];
                                return "";
                            }
                            excludePattern = new int [tmpLine.Length-1];
                            try {
                                for (int i = 1; i < tmpLine.Length; i++) {
                                    excludePattern [i-1] = int.Parse ( tmpLine [i].Trim() );
                                }
                            } catch (Exception e) {
                                Console.Out.WriteLine ( "ERROR PARSING EXCLUDE PATTERN: " + e.Message + "at " + curloopstart );
                                excludePattern = new int [0];
                            }
                        }
                    }
                }
                int endIdx = 0;
                startIdx = nextNewLine(input, startIdx);
                endIdx = beginOfLine(input, input.IndexOf(LOOPEND, startIdx));
                if (endIdx > 0 && endIdx > startIdx) {
                    ret = input.Substring ( startIdx, endIdx - startIdx );
                }
                // read local pattern definition (if any)
                XmlDocument locXML = null;
                int lengthXML = 0;
                localPattern = readTypesXML(ref ret, out lengthXML, ref locXML, true);
                if (readingFromFile) {
                    // read loop from file
                    ret = extractLoopFromFile ( fileName, loopName );
                }
                // set cursor after loop marks 
                curloopstart = nextNewLine ( input, input.IndexOf ( LOOPEND, curloopstart ) );
                return ret; 
            } catch (Exception e) {
                m_message = e.Message;
                return null; 
            }
        }

        private string extractLoopFromFile( string fileName, string loopName ) {
            string inputFile = ""; 
            try {
                if (!Path.IsPathRooted(fileName)) {
                    // find input file, try in current path first
                    inputFile = Path.Combine(m_targetPath, fileName);
                    if (! File.Exists(inputFile)) {
                        // or in *.exe - path? 
                        inputFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory
                                                , fileName);
                    } 
                } else {
                    inputFile = fileName;
                }
                string ret = System.IO.File.ReadAllText(inputFile);
                int curlooppos = ret.IndexOf(LOOPSTART); 
                while (curlooppos >= 0) {
                    string line = fullLine(ret,curlooppos);
                    int startIdx = line.IndexOf(loopName);
                    if (startIdx >= 0 && startIdx < line.Length - loopName.Length
                        && line[startIdx + loopName.Length].CompareTo('@') != 0) {
                        startIdx = nextNewLine(ret, curlooppos); 
                        int endIdx = beginOfLine(ret,ret.IndexOf(LOOPEND, startIdx));
                        if (endIdx > startIdx) {
                            // find / eliminate local XML types 
                            string loopstring = ret.Substring(startIdx, endIdx - startIdx);
                            XmlDocument xmlDoc = null; int lengthXML = 0; 
                            // eliminate xml in read-function
                            readTypesXML(ref loopstring,out lengthXML, ref xmlDoc, true);
                            return loopstring;
                        }
                    }
                    curlooppos = ret.IndexOf(LOOPSTART,curlooppos + 1); 
                }
                return null; 
            } catch (Exception e) {
                Console.Out.WriteLine("Error reading input from file " + fileName + "-> " + e.Message);
                return null;
            }
        }

        /// <summary>
        /// all occurences of any type of locPattern in 'input' will be replaced by their
        /// corresponding replace pattern
        /// </summary>
        /// <param name="input">loop sequence</param>
        /// <param name="destIdx">index into destination string</param>
        /// <returns>loop sequence having all occourences of keys in my pattern
        /// replaced with corresponding destination string out of my patterns (index idx).
        /// </returns>
        private string replaceAllKeys(String input,Types locPattern, int destIdx, ref int tagsReplaced) {
            string ret = "";
            tagsReplaced = 0;
            if (locPattern.CountKey > 0) {
                ret = input;
                foreach (string key in locPattern.Pattern.Keys) {
                    switch (locPattern.Location(key)) {
                        case "after": 
                            // replace first string after pattern with destination 
                            string[] founds = ret.Split(new string[1] { key }, StringSplitOptions.None);
                            StringBuilder sb = new StringBuilder(founds[0]); 
                            for (int i = 1; i < founds.Length; i++) {
                                if (founds[i - 1].LastIndexOf("ENUM:") == founds[i - 1].Length - "ENUM:".Length
                                    && founds[i - 1].Length >= "ENUM:".Length) {
                                    sb.Append(key + founds[i]);
                                    continue;
                                }
                                int startIdx = beginOfPattern(founds[i],0); // new: allow without whitespace! was: nextWord(founds[i], 0); 
                                //startIdx = nextWord(
                                int endIdx = endOfPattern(founds[i],startIdx,locPattern.EndMark[key]); // endOfWord(founds[i],startIdx);
                                if (endIdx > startIdx) {
                                    // do the actual replace ...
                                    founds[i] = founds[i].Remove(startIdx, endIdx - startIdx); 
                                    founds[i] = founds[i].Insert(startIdx, locPattern[key,destIdx]);
                                    sb.Append(founds[i]);
                                    tagsReplaced++; 
                                } else {
                                    sb.Append(founds[i]);
                                }
                            }
                            ret = sb.ToString(); 
                            break; 
                        case "here": 
                            // replace all key strings directly by destination
                            ret = ret.Replace(key, locPattern[key, destIdx]);
                            tagsReplaced++; 
                            break;
                        case "nextline":
                            // replace next line completely
                            int curpos = ret.LastIndexOf(key);
                            while (curpos >= 0) {
                                // check if hycalper tag
                                int eL = endOfLine(ret,curpos);
                                int bL = beginOfLine(ret,curpos);
                                // found
                                int startIdx;
                                int endIdx;
                                if (keep_tags) {
                                    startIdx = nextNewLine(ret, eL);
                                    endIdx = nextNewLine(ret, startIdx); 
                                } else {
                                    startIdx = bL;
                                    endIdx = nextNewLine(ret, nextNewLine(ret, startIdx));
                                }
                                string ind = indent(ret,curpos); 
                                ret = ret.Remove(startIdx, endIdx - startIdx);
                                ret = ret.Insert(startIdx,ind + locPattern[key, destIdx] + Environment.NewLine);
                                curpos = ret.Substring(0, bL).LastIndexOf(key);
                                tagsReplaced++; 
                            }
                            break; 
                        case "endregion":
                            // replace lines (after -> 'keep_tags == true') the line containing the key until line  containing '#endregion HYCALPER' was found
                            curpos = ret.IndexOf(key);
                            while (curpos >= 0) {
                                int startIdx; 
                                int endIdx;
                                string indent = new string (' ',firstNonWhitespace(ret,curpos) - beginOfLine(ret,curpos)); 
                                if (keep_tags) {
                                    startIdx = nextNewLine(ret,curpos);
                                    endIdx = ret.IndexOf("#endregion HYCALPER", startIdx);
                                 } else {
                                    startIdx = beginOfLine(ret, curpos);
                                    endIdx = endOfLine(ret,ret.IndexOf("#endregion HYCALPER",startIdx));
                                 }
                                 if (startIdx > 0 && endIdx > startIdx) {
                                     ret = ret.Remove(startIdx, endIdx - startIdx);
                                     string insert = indent + locPattern[key, destIdx];
                                     ret = ret.Insert(startIdx, insert);
                                     curpos = startIdx + insert.Length; 
                                     tagsReplaced++;
                                 }
                                 curpos = ret.IndexOf(key,curpos);
                            }
                            break;
                        case "comment":
                            // replace text between the FIRST occurence of xml-comment tags <bla> and </bla>. 'bla': name of pattern 
                            string startTag = "<" + key + ">", endTag;  
                            if (!key.StartsWith("param")) 
                                endTag = "</" + key + ">"; 
                            else 
                                endTag = "</param>";
                            curpos = ret.IndexOf(startTag);
                            if (curpos >= 0) {
                                int startIdx = curpos + startTag.Length; 
                                int endIdx = ret.IndexOf(endTag,startIdx); 
                                if (endIdx <= startIdx) 
                                    throw new Exception("missing closing comment tag: " + endTag); 
                                string indent = new string (' ',firstNonWhitespace(ret,startIdx) - beginOfLine(ret,startIdx)); 
                                if (startIdx > 0 && endIdx > startIdx) {
                                     ret = ret.Remove(startIdx, endIdx - startIdx);
                                     string insert = locPattern[key, destIdx].Replace(Environment.NewLine, Environment.NewLine + indent + "/// ");
                                     ret = ret.Insert(startIdx, insert);
                                     tagsReplaced++;
                                }
                            }
                            break;
                    }
                }
            }
            return ret; 
        }

        // the pattern immediately following the found key, remove any whitespace only 
        private int beginOfPattern(string input, int startIdx) {
            int a = startIdx; 
            while (a < input.Length - 1 && input[a] == '*' && input[a+1] == '/') a += 2;   
            while (a < input.Length && char.IsWhiteSpace(input,a)) a++; 
            return a; 
        }

        private int beginOfWord(string ret, int curpos) {
            int a = curpos;
            if (char.IsWhiteSpace(ret,a)) {
                // step into previous word  
                while (a > 0 && ret[a] == ' ') a--;
                if (a == 0) return 0; 
            } 
            while (a > 0 && !char.IsWhiteSpace(ret,a)) a--;
            if (char.IsWhiteSpace(ret[a])) a++;
            return a; 
        }
        private string indent(string input, int curpos) {
            string ret; 
            int a = beginOfWord(input,curpos);
            int lstart = beginOfLine(input,curpos); 
            ret = input.Substring(lstart,a-lstart); 
            ret = ret.Replace("\t","    "); 
            ret = new string(' ',ret.Length); 
            return ret; 
        }

        private string enumAllKeyPatterns(string key) {
            StringBuilder ret = new StringBuilder();
            if (include_secure)
                ret.Append("<![CDATA[");
            string[] pat = Pattern.Pattern[key];
            for (int i = 0; i < pat.Length-1; i++ ) {
                ret.Append(" " + pat[i] + ",");
            }
            if (pat.Length > 1) {
                ret.Append(" " + pat[pat.Length - 1]); 
            }
            if (include_secure)
                ret.Append("]]>");
            return ret.ToString(); 
        }
        private static int beginOfLine(string input, int startIdx) {
            int a = input.Substring(0, startIdx).LastIndexOf(Environment.NewLine);
            if (a < 0) a = 0;
            else a += Environment.NewLine.Length; 
            return a; 
        }
        private static int firstNonWhitespace(string input, int startIdx) {
            int a = beginOfLine(input, startIdx); 
            string content = input.Substring(a, endOfLine(input,startIdx)-a).Trim(); 
            a = input.IndexOf(content,a);
            return a; 
        }
        private static int endOfLine(string input, int startIdx) {
            int a = input.IndexOf(Environment.NewLine, startIdx);
            if (a < 0)
                a = input.Length - 1;
            return a; 
        }
        private static int positionInLine(string input, int startIdx) {
            int a = beginOfLine(input, startIdx);
            return startIdx - a; 
        }
        private static int nextNewLine(string input, int startIdx) {
            int a = endOfLine(input, startIdx);
            if (a < input.Length - 1) a += Environment.NewLine.Length;
            return a; 
        }
        private static int endOfPattern(string input, int startIdx, string endMarkChars) {
            while (startIdx < input.Length && !isEndMark(input[startIdx],endMarkChars)) startIdx++;
            return startIdx; 
        }
        private static bool isEndMark(char input, string endMarkChars) {
            foreach (char a in endMarkChars) 
                if (a == input) return true; 
            return false; 
        }
        private static int endOfWord(string input, int startIdx) {
            int a = startIdx;
            while (a < input.Length && !Char.IsWhiteSpace(input[a])) a++;
            return a; 
        }
        private string fullLine(string input, int index) {
            int end = nextNewLine(input,index); 
            int start = beginOfLine(input,index);
            return input.Substring(start, end - start); 
        }
    }
}
